/*
 * This program is free software: you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation, version 3 of the
 * License.
 *
 * This program is distributed WITHOUT ANY WARRANTY; without even the
 * implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
 * PURPOSE. See the
 *
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>
 */

#include <err.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>

#include "reallocarray.h"
#include "strtonum.h"

#include "arg.h"
#include "config.h"
#include "util.h"

#define MAX_DIAGS 2
#define ARRLEN(a) (sizeof(a) / sizeof((a)[0]))

static const char *red_bold_fg    = "\033[31;1m";
static const char *green_bold_fg  = "\033[32;1m";
static const char *yellow_bold_fg = "\033[33;1m";
static const char *reset          = "\033[m";
static const char *empty_line     = "";

static int byleftside = DEFAULT_BYLEFTSIDE;

char *argv0;

struct item {
	struct tm tm;
	long long v;
	char *c;
	char *d;
	int future;
	int action;
};

struct items {
	struct item **items;
	size_t n;
};

static long long
estrtonum(const char *str, long long minval, long long maxval)
{
	const char *errstr;
	long long result;

	result = strtonum(str, minval, maxval, &errstr);
	if (errstr)
		errx(1, "strtonum: %s '%s'", errstr, str);

	return result;
}

static int
field_tm(char **s, struct item *item)
{
	*s = strptime(*s, "%F %R", &item->tm);
	if (!*s || !*s[0] || *s[0] != '\t')
		return 0;

	(*s)++;

	return 1;
}

static int
field_v(char **s, struct item *item)
{
	char *p;

	p = strchr(*s + 1, '\t');
	if (!p)
		return 0;
	p[0] = '\0';
	if (*s[0] != '-' && *s[0] != '+' && *s[0] != '=')
		return 0;
	item->action = *s[0];
	(*s)++;
	item->v = estrtonum(*s, 0, LLONG_MAX);

	*s = p + 1;

	return 1;
}

static int
field_c(char **s, struct item *item)
{
	size_t slen;
	char *p;

	p = strchr(*s + 1, '\t');
	if (!p)
		return 0;
	p[0] = '\0';

	slen = estrnlen(*s, STRLEN);

	item->c = malloc(slen + sizeof((char)'\0'));
	if (!item->c)
		err(3, "malloc");

	memcpy(item->c, *s, slen + sizeof((char)'\0'));

	*s = p + 1;

	return 1;
}

static int
field_d(char **s, struct item *item)
{
	size_t slen;
	char *p;

	p = strchr(*s + 1, '\n');
	if (!p)
		return 0;
	p[0] = '\0';

	slen = estrnlen(*s, STRLEN);

	item->d = malloc(slen + sizeof((char)'\0'));
	if (!item->c)
		err(3, "malloc");

	memcpy(item->d, *s, slen + sizeof((char)'\0'));

	*s = p + 1;

	return 1;
}

static void
field_flags(struct item *item)
{
	time_t t;
	time_t cur;

	t   = mktime(&item->tm);
	cur = time(NULL);

	if (difftime(cur, t) < 0.0)
		item->future = 1;
	else
		item->future = 0;
}

static int
analize_line(char *s, struct item *item)
{
	if (!field_tm(&s, item))
		return 0;
	if (!field_v(&s, item))
		return 0;
	if (!field_c(&s, item))
		return 0;
	if (!field_d(&s, item))
		return 0;

	field_flags(item);

	return 1;
}

static size_t
fanalize(FILE *f, struct item **items)
{
	char buf[STRLEN + sizeof((char)'\0')];
	size_t bm;

	bm = 0;

	while (fgets(buf, sizeof(buf), f)) {
		if (!strchr(buf, '\n'))
			errx(3, "line was truncated");
		bm++;
		*items = ereallocarray(*items, bm, sizeof(struct item));
		memcpy(&(*items)[bm - 1], &(*items)[bm - 1 - 1],
		       sizeof(struct item));
		if (!analize_line(buf, &(*items)[bm - 1]))
			errx(4, "invalid format");
	}

	if (!feof(f))
		err(2, "fgets");

	return bm;
}

static char *
spaced_string(const char *s, size_t n)
{
	char *res;
	size_t slen;

	res = malloc(n + sizeof((char)'\0'));
	if (!res)
		err(2, "malloc");
	slen = strnlen(s, n);
	if (byleftside) {
		memcpy(res, s, slen);
		memset(res + slen, ' ', n - slen);
	} else {
		memset(res, ' ', n - slen);
		memcpy(res + (n - slen), s, slen);
	}
	res[n] = '\0';

	return res;
}

static void
print_cat(const struct items *cat, size_t strlen)
{
	size_t i;
	struct item **item;
	const char *str;

	unsigned long inc;
	unsigned long dec;
	long long eq;
	long long res;
	long long future;

	inc = dec = eq = res = future = 0;
	for (i = 0, item = cat->items; i < cat->n; i++, item++) {
		switch (item[0]->action) {
			case '-':
				if (item[0]->future)
					future -= (unsigned long)item[0]->v;
				else
					res -= item[0]->v;
				dec += (unsigned long)item[0]->v;
				break;
			case '+':
				if (item[0]->future)
					future += (unsigned long)item[0]->v;
				else
					res += item[0]->v;
				inc += (unsigned long)item[0]->v;
				break;
			case '=':
				res = item[0]->v;
				eq  = item[0]->v;
				break;
		}
	}

	str = spaced_string(cat->items[0]->c, strlen);
	printf(
		"%s:\t=%lld\t%s-%lu%s"
		"\t%s+%lu%s\t%s=%lld%s"
		"\t%s?%lld%s\n",
		str, eq, red_bold_fg, dec, reset, green_bold_fg, inc, reset,
		res < 0 ? red_bold_fg
				: (future ? yellow_bold_fg : green_bold_fg),
		res, reset, (res + future) < 0 ? red_bold_fg : green_bold_fg,
		res + future, reset);
}

static size_t
max(size_t a, size_t b)
{
	if (a > b)
		return a;
	return b;
}

static void
cat_results(struct item *items, size_t itemsn)
{
	struct items *cats;
	struct items *catsp;
	size_t catsn;
	size_t i;
	size_t l;

	catsn = 0;
	cats  = NULL;
	l     = 0;
	for (; itemsn; itemsn--, items++) {
		for (i = catsn, catsp = cats; i; i--, catsp++)
			if (strncmp(catsp->items[0]->c, items->c, STRLEN) == 0)
				break;
		if (!i) {
			catsn++;
			cats  = ereallocarray(cats, catsn, sizeof(struct items));
			catsp = &cats[catsn - 1];
			catsp->n     = 0;
			catsp->items = NULL;
		}

		catsp->n++;
		catsp->items = ereallocarray(catsp->items, catsp->n,
		                             sizeof(struct item *));
		catsp->items[catsp->n - 1] = items;

		l = max(strlen(catsp->items[catsp->n - 1]->c), l);
	}

	for (i = catsn, catsp = cats; i; i--, catsp++)
		print_cat(catsp, l);
}

static int
cmp_dates(struct item *item1, struct item *item2)
{
	if (item1->tm.tm_year == item2->tm.tm_year &&
	    item1->tm.tm_mon == item2->tm.tm_mon)
		return 1;
	return 0;
}

static long long
print_months(struct items *items, long long res)
{
	size_t i;
	struct item **item;

	unsigned long inc;
	unsigned long dec;
	long long eq;
	long long future;

	inc = dec = future = 0;
	eq                 = res;
	for (i = items->n, item = items->items; i; i--, item++) {
		switch (item[0]->action) {
			case '-':
				if (item[0]->future)
					future -= (unsigned long)item[0]->v;
				else {
					res -= item[0]->v;
					dec += (unsigned long)item[0]->v;
				}
				break;
			case '+':
				if (item[0]->future)
					future += (unsigned long)item[0]->v;
				else {
					res += item[0]->v;
					inc += (unsigned long)item[0]->v;
				}
				break;
			case '=':
				res = item[0]->v;
				eq  = item[0]->v;
				break;
		}
	}

	printf("%04d/%02d:\t=%lld\t%s-%lu%s"
	       "\t%s+%lu%s\t%s=%lld%s"
	       "\t%s?%lld%s\n",
	       1900 + items->items[0]->tm.tm_year,
	       items->items[0]->tm.tm_mon, eq, red_bold_fg, dec, reset,
	       green_bold_fg, inc, reset,
	       res < 0 ? red_bold_fg
	               : (future ? yellow_bold_fg : green_bold_fg),
	       res, reset,
	       (res + future) < 0 ? red_bold_fg : green_bold_fg,
	       res + future, reset);

	return res;
}

static void
month_results(struct item *items, size_t itemsn)
{
	struct items *months;
	struct items *monthsp;
	size_t monthsn;
	size_t i;
	long long res;

	months  = NULL;
	monthsn = 0;

	for (; itemsn; itemsn--, items++) {
		for (i = monthsn, monthsp = months; i; i--, monthsp++)
			if (cmp_dates(monthsp->items[0], items))
				break;
		if (!i) {
			monthsn++;
			months =
				ereallocarray(months, monthsn, sizeof(struct items));
			monthsp        = &months[monthsn - 1];
			monthsp->n     = 0;
			monthsp->items = NULL;
		}

		monthsp->n++;
		monthsp->items = ereallocarray(monthsp->items, monthsp->n,
		                               sizeof(struct item *));
		monthsp->items[monthsp->n - 1] = items;
	}

	res = 0;
	for (i = monthsn, monthsp = months; i; i--, monthsp++) {
		res = print_months(monthsp, res);
	}
}

static void
disable_colors(void)
{
	red_bold_fg    = empty_line;
	green_bold_fg  = empty_line;
	yellow_bold_fg = empty_line;
	reset          = empty_line;
}

int
main(int argc, char **argv)
{
	static char *file = DEFAULT_PATH;
	int force_colors;

	struct item *items;
	size_t itemsn;

	static void (*resultsfun[MAX_DIAGS + 1])(struct item *items,
	                                         size_t itemsn);
	static void (**resultsfunp)(struct item *items, size_t itemsn);

	FILE *f;

	force_colors = 0;
	items = NULL;
	memset(resultsfun, '\x00', sizeof(resultsfun));

	resultsfunp = resultsfun;

	ARGBEGIN {
		case 'c':
			*resultsfunp = cat_results;
check_overflow:
			resultsfunp++;
			if (resultsfunp > resultsfun + ARRLEN(resultsfun))
				errx(2, "do not multiply display flags");
			break;
		case 'L':
			byleftside = 0;
			break;
		case 'l':
			byleftside = 1;
			break;
		case 'O':
			disable_colors();
			break;
		case 'o':
			force_colors = 1;
			break;
		case 'm':
			*resultsfunp = month_results;
			goto check_overflow;
		case 'f':
			SHIFT_ARG_IF_DOESNT_EXIST errx(1, "usage: -f file");
			file = argv[0];
			goto next;
		case 'v':
			WRITES(STDOUT_FILENO, "smw-" VERSION"\n");
			return 0;
		default:
			dprintf(STDERR_FILENO, "usage: %s [-cLlmOov] [-f file]",
			        argv0);
			return 0;
	} ARGEND

	if (!resultsfun[0])
		resultsfun[0] = month_results;

	if (!force_colors && !isatty(STDOUT_FILENO))
		disable_colors();

	f = fopen(file, "r");
	if (!f)
		err(2, "fopen: %s", file);

	itemsn = fanalize(f, &items);

	fclose(f);

	for (resultsfunp = resultsfun;;) {
		(*resultsfunp)(items, itemsn);
		resultsfunp++;
		if (resultsfunp[0])
			putchar('\n');
		else
			break;
	}

	free(items);

	return 0;
}
